23장. 테스트를 Agent의 눈으로 쓰기 — 스스로 검증하게 만들기
5장에서 한 줄로 지나간 문장이 있다.
테스트가 없는 프로젝트에서
Agent의 “수정 완료” 는 의견에 불과하다.
이 장은 그 문장을 실무로 옮긴다.
관점 하나만 바꾸면 된다.
테스트는 품질 보증 장치이기 전에
Agent가 세상을 보는 유일한 창이다.
사람과 Agent가 테스트를 쓰는 방식은 다르다
| 사람 | Agent |
|---|---|
| 실패하면 코드를 열어본다 | 실패 메시지만 본다 |
| 애매하면 동료에게 묻는다 | 다음 시도로 넘어간다 |
| 감으로 이상함을 느낀다 | 통과하면 끝났다고 판단한다 |
🔥 세 번째가 핵심이다.
Agent에게 “통과” 는 완료의 근거다.
그래서 테스트가 약하면
Agent는 약한 근거로 자신 있게 완료를 보고한다.
세 가지 용도
에이전틱 코딩에서 테스트는 세 역할을 한다.
1️⃣ 현재 동작을 고정한다
레거시를 고칠 때 가장 먼저 필요한 것이다.
@Test
fun `부분 취소 시 포인트는 비례 배분된다`() {
// 지금 이렇게 동작한다 (옳은지는 별개)
}
옳고 그름을 판단하지 않고 현재를 박제한다.
36장의 Characterization Test다.
2️⃣ 버그를 증명한다
9장에서 본 재현 테스트다.
실패하는 테스트가 있으면
Agent는 목표를 정확히 안다.
3️⃣ 회귀를 막는다
가장 익숙한 용도이고,
Agent 시대에 가치가 몇 배로 올랐다.
사람은 하루 세 번 코드를 고치지만
Agent는 한 시간에 세 번 고친다.
Agent에게 좋은 테스트의 조건
세 가지다.
빠르다
| 테스트 시간 | Agent의 행동 |
|---|---|
| 10초 | 매번 돌린다 |
| 1분 | 자주 돌린다 |
| 4분 | 마지막에 한 번 돌린다 |
| 15분 | 사실상 안 돌린다 |
⚠️ 느린 테스트는 없는 테스트에 수렴한다.
Feedback Loop가 4분마다 한 바퀴 돌면
Agent는 검증 없이 여러 수정을 몰아서 한다.
그래서 실패했을 때 원인을 짚기도 어려워진다.
빠른 단위 테스트를 따로 분리해두는 것이
에이전틱 코딩에서는 큰 투자 대비 효과를 낸다.
./gradlew test --tests '*UnitTest' # 12초
./gradlew test # 4분
결정적이다
가끔 실패하는 테스트는 최악이다.
1차 시도 실패 → Agent가 코드를 고친다
2차 시도 통과 → "고쳐졌습니다"
⚠️ 고친 것이 아니라 운이 좋았던 것인데
Agent는 자기 수정이 효과가 있었다고 학습한다.
그리고 그 잘못된 인과가 Context에 남는다.
시간·랜덤·외부 호출·실행 순서 의존은
Agent와 함께 일하기 전에 정리해야 한다.
실패 메시지가 말을 한다
Agent가 받는 유일한 단서다.
❌ expected: true but was: false
✅ 주문 취소 시 포인트 환급은 한 번만 발생한다
expected size: 1 but was: 2
actual: [PointHistory(+1000, REFUND), PointHistory(+1000, REFUND)]
두 번째를 받은 Agent는
“환급이 두 번 일어났다” 를 즉시 안다.
첫 번째를 받으면 코드를 다시 읽어야 한다.
Context를 더 쓰고, 추측이 늘어난다.
좋은 실패 메시지는
Agent의 Context를 아껴준다.
테스트 작성을 시킬 때
기존 테스트를 먼저 읽히는 것이 순서다.
@src/test/kotlin/order/OrderCancelTest.kt 를 읽고
같은 스타일로 부분 취소 케이스를 추가해줘.
- 새 테스트 기반 클래스를 만들지 마
- 기존 IntegrationTestBase 를 그대로 상속해
- given/when/then 주석 스타일을 유지해
⚠️ 이 지시가 없으면 Agent는 새 스타일을 만든다.
일반적으로 좋은 스타일일 수는 있다.
하지만 테스트 파일마다 스타일이 다른 프로젝트가 된다.
Agent가 테스트를 망가뜨리는 네 가지 패턴
3장에서 예고한 그 문제다.
| 패턴 | 겉보기 | 실제 |
|---|---|---|
| 단정문 삭제 | 통과 | 검증이 사라짐 |
@Disabled 추가 | 통과 | 문제를 미룸 |
| 기댓값을 실제값으로 수정 | 통과 | 버그를 정답으로 승격 |
| 예외를 잡아서 무시 | 통과 | 증상만 감춤 |
세 번째가 가장 자주 일어나고 가장 위험하다.
// Agent의 수정
assertThat(histories).hasSize(1) → assertThat(histories).hasSize(2)
버그가 명세가 됐다.
방지는 세 겹으로 한다.
# CLAUDE.md
- 테스트를 삭제하거나 @Disabled 를 추가하지 않는다
- 실패하는 테스트의 기댓값을 바꾸지 않는다
(기댓값이 틀렸다고 판단되면 수정하지 말고 보고한다)
# 작업 지시
테스트 코드는 수정하지 말고 구현만 고쳐줘
# Diff 검토
git diff --stat src/test/
세 번째가 마지막 방어선이다.
테스트 디렉터리의 변경량만 먼저 본다.
테스트가 아예 없는 영역
레거시에서는 흔한 상황이다.
이때 순서는 이렇다.
flowchart LR
A[현재 동작 파악] --> B[Characterization Test]
B --> C[통과 확인]
C --> D[그 다음에 수정]
⚠️ 테스트 없이 수정부터 하지 않는다.
무엇이 깨졌는지 알 방법이 없는 상태에서
Agent의 속도는 위험이 된다.
36장에서 이 작업을 본격적으로 다룬다.
이 장의 핵심
- 테스트는 Agent가 세상을 보는 유일한 창이다
- Agent에게 “통과” 는 완료의 근거다 — 약한 테스트는 약한 근거를 만든다
- 용도는 셋이다 — 현재 동작 고정, 버그 증명, 회귀 방지
- 느린 테스트는 없는 테스트에 수렴한다 — 빠른 단위 테스트를 분리한다
- 가끔 실패하는 테스트는 Agent에게 잘못된 인과를 학습시킨다
- 좋은 실패 메시지는 Agent의 Context를 아껴준다
- 기존 테스트를 먼저 읽히지 않으면 새 스타일이 생긴다
- 기댓값을 실제값으로 고치는 수정이 가장 위험하다 — 버그가 명세가 된다
- 테스트 디렉터리의 변경량을 따로 확인하는 것이 마지막 방어선이다